using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Runtime.InteropServices;

namespace Voice
{
    public delegate void BufferFillEventHandler(IntPtr data, int size);
    /// <summary>
    /// This class represents wav output device.
    /// </summary>
    public class WavOutDevice
    {
        private int m_Index = 0;
        private string m_Name = "";
        private int m_Channels = 1;

        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <param name="index">Device index in devices.</param>
        /// <param name="name">Device name.</param>
        /// <param name="channels">Number of audio channels.</param>
        internal WavOutDevice(int index, string name, int channels)
        {
            m_Index = index;
            m_Name = name;
            m_Channels = channels;
        }


        #region Properties Implementation

        /// <summary>
        /// Gets device name.
        /// </summary>
        public string Name
        {
            get { return m_Name; }
        }

        /// <summary>
        /// Gets number of output channels(mono,stereo,...) supported.
        /// </summary>
        public int Channels
        {
            get { return m_Channels; }
        }


        /// <summary>
        /// Gets device index in devices.
        /// </summary>
        internal int Index
        {
            get { return m_Index; }
        }

        #endregion

    }
	internal class WaveOutHelper
	{
		public static void Try(int err)
		{
			if (err != WINMM_DLL.MMSYSERR_NOERROR)
				throw new Exception(err.ToString());
		}
	}

	internal class WaveOutBuffer : IDisposable
	{
		public WaveOutBuffer NextBuffer;

		private AutoResetEvent m_PlayEvent = new AutoResetEvent(false);
		private IntPtr m_WaveOut;

		private WINMM_DLL.WaveHdr m_Header;
		private byte[] m_HeaderData;
		private GCHandle m_HeaderHandle;
		private GCHandle m_HeaderDataHandle;

		private bool m_Playing;

		internal static void WaveOutProc(IntPtr hdrvr, int uMsg, int dwUser, ref WINMM_DLL.WaveHdr wavhdr, int dwParam2)
		{
			if (uMsg == WINMM_DLL.MM_WOM_DONE)
			{
				try
				{
					GCHandle h = (GCHandle)wavhdr.dwUser;
					WaveOutBuffer buf = (WaveOutBuffer)h.Target;
					buf.OnCompleted();
				}
				catch
				{
				}
			}
		}

		public WaveOutBuffer(IntPtr waveOutHandle, int size)
		{
			m_WaveOut = waveOutHandle;

			m_HeaderHandle = GCHandle.Alloc(m_Header, GCHandleType.Pinned);
			m_Header.dwUser = (IntPtr)GCHandle.Alloc(this);
			m_HeaderData = new byte[size];
			m_HeaderDataHandle = GCHandle.Alloc(m_HeaderData, GCHandleType.Pinned);
			m_Header.lpData = m_HeaderDataHandle.AddrOfPinnedObject();
			m_Header.dwBufferLength = size;
			WaveOutHelper.Try(WINMM_DLL.waveOutPrepareHeader(m_WaveOut, ref m_Header, Marshal.SizeOf(m_Header)));
		}
		~WaveOutBuffer()
		{
			Dispose();
		}
		public void Dispose()
		{
			if (m_Header.lpData != IntPtr.Zero)
			{
				WINMM_DLL.waveOutUnprepareHeader(m_WaveOut, ref m_Header, Marshal.SizeOf(m_Header));
				m_HeaderHandle.Free();
				m_Header.lpData = IntPtr.Zero;
			}
			m_PlayEvent.Close();
			if (m_HeaderDataHandle.IsAllocated)
				m_HeaderDataHandle.Free();
			GC.SuppressFinalize(this);
		}

		public int Size
		{
			get { return m_Header.dwBufferLength; }
		}

		public IntPtr Data
		{
			get { return m_Header.lpData; }
		}

		public bool Play()
		{
			lock(this)
			{
				m_PlayEvent.Reset();
				m_Playing = WINMM_DLL.waveOutWrite(m_WaveOut, ref m_Header, Marshal.SizeOf(m_Header)) == WINMM_DLL.MMSYSERR_NOERROR;
				return m_Playing;
			}
		}
		public void WaitFor()
		{
			if (m_Playing)
			{
				m_Playing = m_PlayEvent.WaitOne();
			}
			else
			{
				Thread.Sleep(0);
			}
		}
		public void OnCompleted()
		{
			m_PlayEvent.Set();
			m_Playing = false;
		}
	}

	public class WaveOutPlayer : IDisposable, INotifyPropertyChanged
	{
		private IntPtr m_WaveOut;
		private WaveOutBuffer m_Buffers; // linked list
		private WaveOutBuffer m_CurrentBuffer;
		private Thread m_Thread;
		private BufferFillEventHandler m_FillProc;
		private bool m_Finished;
		private byte m_zero;

        public event PropertyChangedEventHandler PropertyChanged = delegate { };

        /// <summary>
        /// Gets all available output audio devices.
        /// </summary>
        public static WavOutDevice[] Devices
        {
            get
            {
                List<WavOutDevice> retVal = new List<WavOutDevice>();
                // Get all available output devices and their info.
                int devicesCount = WINMM_DLL.waveOutGetNumDevs();
                for (int i = 0; i < devicesCount; i++)
                {
                    WAVEOUTCAPS pwoc = new WAVEOUTCAPS();
                    if (WINMM_DLL.waveOutGetDevCaps((uint)i, ref pwoc, Marshal.SizeOf(pwoc)) == MMSYSERR.NOERROR)
                    {
                        retVal.Add(new WavOutDevice(i, pwoc.szPname, pwoc.wChannels));
                    }
                }

                return retVal.ToArray();
            }
        }

		private WINMM_DLL.WaveDelegate m_BufferProc = new WINMM_DLL.WaveDelegate(WaveOutBuffer.WaveOutProc);

		public static int DeviceCount
		{
			get { return WINMM_DLL.waveOutGetNumDevs(); }
		}
        private int SaveBufSize;
        private int SaveBufCount;
        private WavOutDevice _devRecordOut;
        public WavOutDevice DevRecordOut
        {
            get { return _devRecordOut; }
            set
            {
                if (_devRecordOut == value) return;
                _devRecordOut = value;

                Dispose();
                m_Finished = false;
                if (_devRecordOut != null)
                {
                    WaveOutHelper.Try(WINMM_DLL.waveOutOpen(out m_WaveOut, _devRecordOut.Index, WaveOutFormat, m_BufferProc, 0, WINMM_DLL.CALLBACK_FUNCTION));
                    AllocateBuffers(SaveBufSize, SaveBufCount);

                    m_Thread = new Thread(new ThreadStart(ThreadProc));
                    m_Thread.Start();
                    PropertyChanged(this, new PropertyChangedEventArgs("FilePath"));
                }

            }
        }

		public WaveOutPlayer(int device, WaveFormat format, int bufferSize, int bufferCount, BufferFillEventHandler fillProc)
		{
			m_zero = format.wBitsPerSample == 8 ? (byte)128 : (byte)0;
			m_FillProc = fillProc;
			WaveOutHelper.Try(WINMM_DLL.waveOutOpen(out m_WaveOut, device, format, m_BufferProc, 0, WINMM_DLL.CALLBACK_FUNCTION));
			AllocateBuffers(bufferSize, bufferCount);
		    SaveBufSize = bufferSize;
		    SaveBufCount = bufferCount;
            m_Thread = new Thread(new ThreadStart(ThreadProc));
			m_Thread.Start();
		}

	    private WaveFormat WaveOutFormat;
        public WaveOutPlayer(WavInDevice device, WaveFormat format, int bufferSize, int bufferCount)
        {
            SaveBufSize = bufferSize;
            SaveBufCount = bufferCount;
            WaveOutHelper.Try(WINMM_DLL.waveOutOpen(out m_WaveOut, device.Index, format, m_BufferProc, 0, WINMM_DLL.CALLBACK_FUNCTION));
            WaveOutFormat = format;
            AllocateBuffers(bufferSize, bufferCount);

            m_Thread = new Thread(new ThreadStart(ThreadProc));
            m_Thread.Start();
        }


		~WaveOutPlayer()
		{
			Dispose();
		}
		public void Dispose()
		{
			if (m_Thread != null)
				try
				{
					m_Finished = true;
					if (m_WaveOut != IntPtr.Zero)
						WINMM_DLL.waveOutReset(m_WaveOut);
					m_Thread.Join();
					m_FillProc = null;
					FreeBuffers();
					if (m_WaveOut != IntPtr.Zero)
						WINMM_DLL.waveOutClose(m_WaveOut);
				}
				finally
				{
					m_Thread.Abort();
					m_Thread = null;
					m_WaveOut = IntPtr.Zero;
				}
			GC.SuppressFinalize(this);
		}
		private void ThreadProc()
		{
			while (!m_Finished)
			{
				Advance();
				if (m_FillProc != null && !m_Finished)
					m_FillProc(m_CurrentBuffer.Data, m_CurrentBuffer.Size);
				else
				{
					// zero out buffer
					byte v = m_zero;
					byte[] b = new byte[m_CurrentBuffer.Size];
					for (int i = 0; i < b.Length; i++)
						b[i] = v;
					Marshal.Copy(b, 0, m_CurrentBuffer.Data, b.Length);

				}
				m_CurrentBuffer.Play();
			}
			WaitForAllBuffers();
		}
		private void AllocateBuffers(int bufferSize, int bufferCount)
		{
			FreeBuffers();
			if (bufferCount > 0)
			{
				m_Buffers = new WaveOutBuffer(m_WaveOut, bufferSize);
				WaveOutBuffer Prev = m_Buffers;
				try
				{
					for (int i = 1; i < bufferCount; i++)
					{
						WaveOutBuffer Buf = new WaveOutBuffer(m_WaveOut, bufferSize);
						Prev.NextBuffer = Buf;
						Prev = Buf;
					}
				}
				finally
				{
					Prev.NextBuffer = m_Buffers;
				}
			}
		}
		private void FreeBuffers()
		{
			m_CurrentBuffer = null;
			if (m_Buffers != null)
			{
				WaveOutBuffer First = m_Buffers;
				m_Buffers = null;

				WaveOutBuffer Current = First;
				do
				{
					WaveOutBuffer Next = Current.NextBuffer;
					Current.Dispose();
					Current = Next;
				} while(Current != First);
			}
		}
		private void Advance()
		{
			m_CurrentBuffer = m_CurrentBuffer == null ? m_Buffers : m_CurrentBuffer.NextBuffer;
			m_CurrentBuffer.WaitFor();
		}
		private void WaitForAllBuffers()
		{
			WaveOutBuffer Buf = m_Buffers;
			while (Buf.NextBuffer != m_Buffers)
			{
				Buf.WaitFor();
				Buf = Buf.NextBuffer;
			}
		}
	}
}
